feat(mix): add content-sized GridTrack.auto() rows - #1031
Merged
Conversation
GridBox could not express a grid whose rows size to their tallest child.
The only workaround, autoRows(GridTrack.fixed(n)), silently truncated any
child taller than the declared track, so a scroll view of unknown-height
content had no correct option.
Add GridTrack.auto(), valid in rows and autoRows, and default an omitted
autoRows to it so implicit rows match CSS's grid-auto-rows: auto. Columns
still reject auto; content-sized columns need a separate two-axis design.
RenderMixGrid keeps the one-pass path for fixed/fr grids. When any
effective row is auto, children in those rows are measured at their
resolved column width with a loose height, each row takes its tallest
measurement, and the final pass lays every child into the stretched cell.
Live layout, dry layout, and vertical intrinsics share that geometry
through Flutter's ChildLayouter helpers, following RenderTable's pattern.
The wire format gains a fieldless {"type":"auto"} track as an additive v1
discriminator, accepted on rows and autoRows only.
BREAKING CHANGE: a GridBox that needed more rows than it declared used to
throw; it now lays out with content-sized implicit rows. This applies both
when no rows are declared and when explicit rows undershoot the child
count.
Three CI failures from the auto-rows change: - dart format left two over-long isNot(contains(...)) assertions in grid_box_test.dart unwrapped. - DCM's arguments-ordering rule requires child/children last (lints_with_dcm.yaml), but _measureAutoRowChild declared and passed child first. - mix_chart_protocol composes the core vocabulary, so its exported v1 schema fingerprint moved when the grid_box branch gained the auto track kind. Verified the delta is confined to grid_box: "auto" appears in exactly one of the 26 exported branches.
Merged
5 tasks
A fully tight cell constraint made every child its own relayout boundary, so a child-only change never reached RenderMixGrid and the auto row kept the height measured on its first layout pass. Give auto-row children a loose max height with parentUsesSize so the row re-measures, and leave fixed/fr rows on the tight cell that is their documented contract. Carry the resolved row tracks on GridLayoutResult so performLayout can tell the two cases apart, and inline the single-use measure helper.
Move the GridTrack design rationale out of the published dartdoc into a maintainer comment: it named a private helper and argued against a future refactor, neither of which belongs on pub.dev. Replace the only curly quotes in the Dart sources, rewrite two awkward autoRows doc sentences, drop the stringified Object parameter on the auto-row error helper, and rewrap two markdown lines that broke the wrap width their files otherwise keep.
… row An auto-row child was always laid out a second time, because the final constraint differed from the measure-pass one only in minHeight (0 -> row height). RenderObject.layout could not take its early return, so the whole subtree relaid out even when the measure already produced the row height. Hand a child that already fills its row the exact constraint it was measured with. That covers at least the tallest child of every auto row and every child of a single-child row, so nested auto grids stop multiplying layout passes per level: a leaf inside three nested auto Grids goes from 8 relayouts to 1. Only children shorter than the row are laid out again to stretch them. The relayout-boundary rationale moves into _cellConstraints, with a note to keep the constraint identical to the measure-pass one. _RenderLayoutCallCounter now counts performLayout executions, not just layout calls; the old assertion stayed green through the extra pass because layout() is still called twice, it just early-returns now. Also correct the CHANGELOG, doc/grid-layout.md, and the layout skill reference, which documented the 2^n nesting cost as inherent, and mark the unreachable fractional branch in _computeAutoRowIntrinsicHeight as such.
The existing relayout test drives height from a parent rebuild, which reaches the grid through the widget tree whatever constraint the cell carries. That does not isolate the invariant it claims to protect. This one changes height entirely below the grid via the child's own setState, with GridBoxSpec untouched so MixGrid.updateRenderObject no-ops. The only route back to the grid is markNeedsLayout propagating past the child, which a tight cell would stop by making the child its own relayout boundary. That models the real reported case: an async image or an expanding tile inside a scroll view. Verified as a real guard: restoring the pre-fix constraint (tight cell, parentUsesSize false) fails it with the row stuck at its first measured height, 30 where 90 is expected.
The feature entry hedged ("may be measured and then stretched") and repeated
the layout-pass accounting that the breaking-change entry also carried, while
neither said what an auto row actually does or when to reach for one.
State the behaviour once, in the feature entry, and give the breaking entry
what a breaking note is for: the migration. A reader who relied on the throw to
catch an under-declared Grid now gets told to declare rows or set autoRows.
Also drop a redundant clause from _cellConstraints: a single-child row's only
child is by definition its tallest, so naming both cases said one thing twice.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issue
None filed. Reported directly against
main@390421bewith a reproduction; investigation notes are in.context/grid-content-sized-tracks-findings.md.Description
GridBoxcould not express a grid whose rows size to their tallest child, and the only available workaround silently truncated content.Given a 2-column
GridBoxwith fourTextchildren of unequal height inside aSingleChildScrollViewat width 400:autoRows→ threwGrid auto-placement requires an autoRows track.autoRows(.fr(1))→ threwGrid autoRows requires a bounded height for fractional tracks.autoRows(.fixed(40))→ no error, but the long cell was laid out atSize(194, 40)while the same text at the same width needsSize(194, 300)The third case is the important one:
performLayoutgave every child a tight cell, so a child taller than its track was clipped with no diagnostic — unlikeColumn/Wrap, which report visible overflow. Every shipped grid example worked around this with a hardcoded row height (.fixed(116),(104),(230),(210),(220),(190)), so the gallery could only demonstrate content whose height was already known. CSS Grid's defaultgrid-auto-rowsisauto; Mix could not express the CSS default.This adds
GridTrack.auto()and makes it the default for implicit rows.No stock Flutter widget satisfied the contract.
Tablehas the right row-height rule but not GridBox's gaps, fixed/frrow semantics, or flat-child identity across responsive column changes;Wraplacks shared tracks and stretch; sliver grids require delegate-chosen tile heights;CustomMultiChildLayoutcannot size the parent from a measure-then-stretch pass.RenderMixGridstays the owner and borrowsRenderTable's algorithm.Changes
Public API
GridTrack.auto()— a fieldless, content-sized track, valid inrowsandautoRows. Columns reject it with a vertical-only hint.autoRowsnow resolves toGridTrack.auto(), so a Grid with columns and no row declaration sizes each implicit row to its tallest child.autointerpolates withauto(constant);auto↔ numeric track kinds snap at the animation midpoint like other incompatible kinds.Layout (
render_grid.dart)frgrids keep the existing single-pass path —_hasAutoTrackshort-circuits the measure pass, so those children are still laid out once.ChildLayoutHelper.layoutChild/dryLayoutChild.computeGridLayoutnow takes already-resolved rows, so implicit-row expansion happens exactly once per layout.Protocol
{"type":"auto"}nested discriminator, gated torows,autoRows, and constraint patches. Auto tracks carry no token references. The exported schema fingerprint changes accordingly.Docs & example
doc/grid-layout.mddocuments the row-only scope, the default for implicit rows, the bounded-height rule forfrrows, why fixed tracks clip by design, and the nesting cost.Review Checklist
packages/mix2912 tests,packages/mix/example15,packages/mix_protocol393 — all passing.dart analyzeclean on all three. New coverage includes the reported scroll-view repro, mixed auto+fixed and auto+frrows under loose and tight bounded height, responsive remeasurement across anonConstraintsbreakpoint without rebuilding children, a layout-count assertion proving only auto-row children pay the second pass, live/dry/intrinsic parity, animation, seeded property tests over 200 random grids, and protocol round-trips including lenient decode of an unrecognized track.packages/mix/CHANGELOG.mdunder### Breaking changes. See below.doc/grid-layout.md,README.md,example/README.md,mix_protocol/GUIDE.md,mix_protocol/WIRE_CONTRACT.md, and the Grid guidance underskills/mix/.btwld/mix-docsneeds a matching update for the new track kind and the implicit-row default.Additional Information
Breaking change. A
GridBoxthat needed more rows than it declared used to throw; it now lays out with content-sized implicit rows. This applies both when no rows are declared and when an explicitrowslist undershoots the child count. No currently-successful fixed/frlayout changes.Deliberately not included: a universal overflow warning for fixed cells. A fixed track is a hard constraint in the same way a tight
SizedBoxis, and a reliable generic probe would need a speculative measure pass on every fixed child — which also breaks children that require a bounded height. Now thatautoexists, fixed rows stay strict, and the docs say so explicitly.Cost. Auto-row children are laid out twice (measure, then stretch). Nesting compounds this multiplicatively — a leaf inside three nested auto Grids is laid out eight times. This is inherent to the measure-then-stretch algorithm (
RenderTablehas it too), and is documented in both the guide and the changelog.Scope. Content-sized columns are deliberately out. Width-dependent wrapping creates feedback between column and row sizing, and the reported use case needs content-height rows after responsive column widths are known.
GridTrack.auto()should not be quietly broadened to columns later without specifying those feedback rules — the reasoning for keeping oneGridTracktype rather than splitting per axis is recorded as a comment ingrid_track.dart.Wire compatibility.
{"type":"auto"}is additive within v1, perWIRE_CONTRACT.md. Old strict decoders fail closed; lenient decoders warn and drop the smallest unrecognized value (now covered by a test). Payloads containingtype: autoshould only be produced once matchingmixandmix_protocolversions ship together.